package com.haredot.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import io.lettuce.core.ReadFrom;
import org.springframework.boot.autoconfigure.cache.CacheProperties;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisSentinelConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;

import java.util.HashSet;

/**
 * 在使用 Redis的时候，可能会遇到的问题
 * <p>
 * a) 缓存穿透
 * 当访问一个请求的时候，查询数据库的结果为空，而 redis 没有将 空值 进行缓存，导致该请求会绕过缓存
 * 这种现象 被 称为缓存穿透。
 * <p>
 * 解决方案： a)对空 进行缓存， b）布隆过滤器
 * <p>
 * b) 缓存击穿
 * 在 某一个时间点，有大量请求进入系统，此时 缓存的键大量失效 突然过期，导致大量请求进入到了数据库
 * <p>
 * 解决方案：a) 键永不过期，  b) 将键的过期时间 不均匀进行分配。
 * <p>
 * c) 缓存雪崩
 */

@EnableCaching
@Configuration
@EnableConfigurationProperties({CacheProperties.class, RedisProperties.class})
public class RedisConfig {


    @Bean("redisTemplate")
    @ConditionalOnProperty(prefix = "spring.redis.sentinel", value = "master")
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<Object, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }

    @Primary
    @Bean("stringRedisTemplate-sentinel")
    @ConditionalOnProperty(prefix = "spring.redis.sentinel", value = "master")
    public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
        return new StringRedisTemplate(redisConnectionFactory);
    }

    /**
     * 单机版 master
     *
     * @return
     */
    @Bean("stringRedisTemplate")
    @ConditionalOnProperty(prefix = "spring.redis.sentinel", value = "master")
    public StringRedisTemplate stringRedisTemplate(RedisProperties redisProperties) {

        RedisSentinelConfiguration redisSentinelConfiguration = new RedisSentinelConfiguration(redisProperties.getSentinel().getMaster(), new HashSet<>(redisProperties.getSentinel().getNodes()));

        redisSentinelConfiguration.setPassword(redisProperties.getPassword());
        redisSentinelConfiguration.setDatabase(redisProperties.getDatabase());

        LettuceClientConfiguration lettuceClientConfiguration = LettuceClientConfiguration.builder().build();

        LettuceConnectionFactory connectionFactory = new LettuceConnectionFactory(redisSentinelConfiguration, lettuceClientConfiguration);
        connectionFactory.afterPropertiesSet(); // 手动调用该方法，完成对象的初始化操作

        return new StringRedisTemplate(connectionFactory);
    }


    @Bean("redisConnectionFactory")
    @ConditionalOnProperty(prefix = "spring.redis.sentinel", value = "master")
    public RedisConnectionFactory redisConnectionFactory(RedisProperties redisProperties) {

        RedisSentinelConfiguration redisSentinelConfiguration = new RedisSentinelConfiguration(redisProperties.getSentinel().getMaster(), new HashSet<>(redisProperties.getSentinel().getNodes()));

        redisSentinelConfiguration.setPassword(redisProperties.getPassword());
        redisSentinelConfiguration.setDatabase(redisProperties.getDatabase());

        LettuceClientConfiguration lettuceClientConfiguration = LettuceClientConfiguration.builder()
                // 配置读写分离的方式， 默认从 master 节点读取
                .readFrom(ReadFrom.REPLICA_PREFERRED)  // 尽可能 从 slave 节点读取数据，只有当所有的slave 都不可用，才尝试用master 来读
                .build();
        LettuceConnectionFactory connectionFactory = new LettuceConnectionFactory(redisSentinelConfiguration, lettuceClientConfiguration);

        return connectionFactory;
    }

    /**
     * 配置通用 JSON序列化器， 能支持动态数据类型的转换
     */
    @Bean
    public RedisSerializer redisSerializer(ObjectMapper objectMapper) {
        return new GenericJackson2JsonRedisSerializer(objectMapper);
    }

}
